from collections.abc import Callable, Iterable
from typing import Any, Generic, TypeVar

import dash

AnyItem = TypeVar('AnyItem')


class Checklist(dash.html.Div, Generic[AnyItem]):
    def __init__(self, *,
        id: str,
        items_triggers: Iterable[dash.Input],
        items_func: Callable[[], Iterable[AnyItem]],
        status_triggers: Iterable[dash.Input] = (),
        status_func: Callable[[], str] | None = None,
        item_label_func: Callable[[AnyItem], Any],
        label: str | Any,
        style: dict,
    ):
        super().__init__(
            style={
                'display': 'flex',
                'flexDirection': 'column',
                'justifyContent': 'begin',
                **style},
            children=[
                version_store := dash.dcc.Store(
                    id=f'{id}/version',
                    data=0),
                dash.html.Div(
                    style={
                        'flex': '0',
                        'padding': '0.3rem 0.5rem',
                        'display': 'flex',
                        'flexDirection': 'row',
                        'alignItems': 'center',
                    },
                    children=[
                        label,
                        dash.html.Div(style={'flex': '1'}),
                        status_label := dash.html.Span(
                            children='loading',
                            style={'color': '#e0e0e0'}),
                    ]),
                dash.html.Div(
                    style={
                        'flex': '0 0 1px',
                        'backgroundColor': '#e0e0e0',
                    }),
                checklist := dash.dcc.Checklist(
                    id=f"{id}/checklist",
                    options=(), value=(),
                    style={
                        'flex': '1',
                        'height': '0',
                        'overflow': 'auto',
                        'padding': '0rem 0.5rem 0.5rem 0.5rem',
                        'whiteSpace': 'nowrap',
                        'textOverflow': 'ellipsis',
                    })
            ])

        self._selected_items = []
        self._selection_cache = {}
        self._checklist = checklist
        self._version_store = version_store

        if status_func is not None:
            @dash.callback(
                dash.Output(status_label, 'children'),
                *status_triggers)
            def update_status(*_):
                status = status_func()
                if status == 'ready':
                    return ""
                if status == 'loading':
                    return "loading"
                if status == 'error':
                    return "error"
                raise ValueError(f"Unexpected status: {status}")

        @dash.callback(
            dash.Output(checklist, 'options'),
            dash.Output(checklist, 'value'),
            *items_triggers)
        def update_checklist(*_):
            options = []
            selected_options_i = []
            for i, item in enumerate(items_func()):
                label = item_label_func(item)
                option = dict(value=i, label=label)
                options.append(option)
                option_selected = self._selection_cache.setdefault(item, True)
                if option_selected:
                    selected_options_i.append(i)
            return options, selected_options_i

        @dash.callback(
            dash.Output(version_store, 'data'),
            dash.Input(checklist, 'value'),
            dash.State(version_store, 'data'))
        def update_selection(selected_options_i, version):
            items = list(items_func())
            self._selected_items.clear()
            for i in selected_options_i:
                item = items[i]
                self._selection_cache[item] = True
                self._selected_items.append(item)
            for i, item in enumerate(items):
                if i not in selected_options_i:
                    self._selection_cache[item] = False
            return version + 1

    @property
    def selected_items(self) -> list[AnyItem]:
        return self._selected_items

    @property
    def trigger(self) -> dash.Input:
        return dash.Input(self._version_store, 'data')
